/*
 * ProGuard -- shrinking, optimization, obfuscation, and preverification of Java bytecode.
 *
 * Copyright (c) 2002-2018 GuardSquare NV
 *
 * This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public
 * License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later
 * version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
 * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along with this program; if not, write to the Free
 * Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 */
package proguard.classfile.instruction;

import proguard.classfile.*;
import proguard.classfile.attribute.CodeAttribute;
import proguard.classfile.instruction.visitor.InstructionVisitor;

/**
 * This Instruction represents an instruction that refers to a variable on the local variable stack.
 *
 * @author Eric Lafortune
 */
public class VariableInstruction extends Instruction {
    public boolean wide;
    public int variableIndex;
    public int constant;

    /**
     * Creates an uninitialized VariableInstruction.
     */
    public VariableInstruction() {}

    public VariableInstruction(boolean wide) {
        this.wide = wide;
    }

    public VariableInstruction(byte opcode) {
        this(opcode, embeddedVariable(opcode), 0);
    }

    public VariableInstruction(byte opcode, int variableIndex) {
        this(opcode, variableIndex, 0);
    }

    public VariableInstruction(byte opcode, int variableIndex, int constant) {
        this.opcode = opcode;
        this.variableIndex = variableIndex;
        this.constant = constant;
        this.wide = requiredVariableIndexSize() > 1 || requiredConstantSize() > 1;
    }

    /**
     * Copies the given instruction into this instruction.
     * 
     * @param variableInstruction the instruction to be copied.
     * @return this instruction.
     */
    public VariableInstruction copy(VariableInstruction variableInstruction) {
        this.opcode = variableInstruction.opcode;
        this.variableIndex = variableInstruction.variableIndex;
        this.constant = variableInstruction.constant;
        this.wide = variableInstruction.wide;

        return this;
    }

    /**
     * Return the embedded variable of the given opcode, or 0 if the opcode doesn't have one.
     */
    private static int embeddedVariable(byte opcode) {
        switch (opcode) {
            case InstructionConstants.OP_ILOAD_1:
            case InstructionConstants.OP_LLOAD_1:
            case InstructionConstants.OP_FLOAD_1:
            case InstructionConstants.OP_DLOAD_1:
            case InstructionConstants.OP_ALOAD_1:
            case InstructionConstants.OP_ISTORE_1:
            case InstructionConstants.OP_LSTORE_1:
            case InstructionConstants.OP_FSTORE_1:
            case InstructionConstants.OP_DSTORE_1:
            case InstructionConstants.OP_ASTORE_1:
                return 1;

            case InstructionConstants.OP_ILOAD_2:
            case InstructionConstants.OP_LLOAD_2:
            case InstructionConstants.OP_FLOAD_2:
            case InstructionConstants.OP_DLOAD_2:
            case InstructionConstants.OP_ALOAD_2:
            case InstructionConstants.OP_ISTORE_2:
            case InstructionConstants.OP_LSTORE_2:
            case InstructionConstants.OP_FSTORE_2:
            case InstructionConstants.OP_DSTORE_2:
            case InstructionConstants.OP_ASTORE_2:
                return 2;

            case InstructionConstants.OP_ILOAD_3:
            case InstructionConstants.OP_LLOAD_3:
            case InstructionConstants.OP_FLOAD_3:
            case InstructionConstants.OP_DLOAD_3:
            case InstructionConstants.OP_ALOAD_3:
            case InstructionConstants.OP_ISTORE_3:
            case InstructionConstants.OP_LSTORE_3:
            case InstructionConstants.OP_FSTORE_3:
            case InstructionConstants.OP_DSTORE_3:
            case InstructionConstants.OP_ASTORE_3:
                return 3;

            default:
                return 0;
        }
    }

    /**
     * Returns whether this instruction stores the value of a variable. The value is false for the ret instruction, but
     * true for the iinc instruction.
     */
    public boolean isStore() {
        // A store instruction can be recognized as follows. Note that this
        // excludes the ret instruction, which has a negative opcode.
        return opcode >= InstructionConstants.OP_ISTORE || opcode == InstructionConstants.OP_IINC;
    }

    /**
     * Returns whether this instruction loads the value of a variable. The value is true for the ret instruction and for
     * the iinc instruction.
     */
    public boolean isLoad() {
        // A load instruction can be recognized as follows. Note that this
        // includes the ret instruction, which has a negative opcode.
        return opcode < InstructionConstants.OP_ISTORE;
    }

    // Implementations for Instruction.

    public byte canonicalOpcode() {
        // Remove the _0, _1, _2, _3 extension, if any.
        switch (opcode) {
            case InstructionConstants.OP_ILOAD_0:
            case InstructionConstants.OP_ILOAD_1:
            case InstructionConstants.OP_ILOAD_2:
            case InstructionConstants.OP_ILOAD_3:
                return InstructionConstants.OP_ILOAD;
            case InstructionConstants.OP_LLOAD_0:
            case InstructionConstants.OP_LLOAD_1:
            case InstructionConstants.OP_LLOAD_2:
            case InstructionConstants.OP_LLOAD_3:
                return InstructionConstants.OP_LLOAD;
            case InstructionConstants.OP_FLOAD_0:
            case InstructionConstants.OP_FLOAD_1:
            case InstructionConstants.OP_FLOAD_2:
            case InstructionConstants.OP_FLOAD_3:
                return InstructionConstants.OP_FLOAD;
            case InstructionConstants.OP_DLOAD_0:
            case InstructionConstants.OP_DLOAD_1:
            case InstructionConstants.OP_DLOAD_2:
            case InstructionConstants.OP_DLOAD_3:
                return InstructionConstants.OP_DLOAD;
            case InstructionConstants.OP_ALOAD_0:
            case InstructionConstants.OP_ALOAD_1:
            case InstructionConstants.OP_ALOAD_2:
            case InstructionConstants.OP_ALOAD_3:
                return InstructionConstants.OP_ALOAD;

            case InstructionConstants.OP_ISTORE_0:
            case InstructionConstants.OP_ISTORE_1:
            case InstructionConstants.OP_ISTORE_2:
            case InstructionConstants.OP_ISTORE_3:
                return InstructionConstants.OP_ISTORE;
            case InstructionConstants.OP_LSTORE_0:
            case InstructionConstants.OP_LSTORE_1:
            case InstructionConstants.OP_LSTORE_2:
            case InstructionConstants.OP_LSTORE_3:
                return InstructionConstants.OP_LSTORE;
            case InstructionConstants.OP_FSTORE_0:
            case InstructionConstants.OP_FSTORE_1:
            case InstructionConstants.OP_FSTORE_2:
            case InstructionConstants.OP_FSTORE_3:
                return InstructionConstants.OP_FSTORE;
            case InstructionConstants.OP_DSTORE_0:
            case InstructionConstants.OP_DSTORE_1:
            case InstructionConstants.OP_DSTORE_2:
            case InstructionConstants.OP_DSTORE_3:
                return InstructionConstants.OP_DSTORE;
            case InstructionConstants.OP_ASTORE_0:
            case InstructionConstants.OP_ASTORE_1:
            case InstructionConstants.OP_ASTORE_2:
            case InstructionConstants.OP_ASTORE_3:
                return InstructionConstants.OP_ASTORE;

            default:
                return opcode;
        }
    }

    public Instruction shrink() {
        opcode = canonicalOpcode();

        // Is this instruction pointing to a variable with index from 0 to 3?
        if (variableIndex <= 3) {
            switch (opcode) {
                case InstructionConstants.OP_ILOAD:
                    opcode = (byte)(InstructionConstants.OP_ILOAD_0 + variableIndex);
                    break;
                case InstructionConstants.OP_LLOAD:
                    opcode = (byte)(InstructionConstants.OP_LLOAD_0 + variableIndex);
                    break;
                case InstructionConstants.OP_FLOAD:
                    opcode = (byte)(InstructionConstants.OP_FLOAD_0 + variableIndex);
                    break;
                case InstructionConstants.OP_DLOAD:
                    opcode = (byte)(InstructionConstants.OP_DLOAD_0 + variableIndex);
                    break;
                case InstructionConstants.OP_ALOAD:
                    opcode = (byte)(InstructionConstants.OP_ALOAD_0 + variableIndex);
                    break;

                case InstructionConstants.OP_ISTORE:
                    opcode = (byte)(InstructionConstants.OP_ISTORE_0 + variableIndex);
                    break;
                case InstructionConstants.OP_LSTORE:
                    opcode = (byte)(InstructionConstants.OP_LSTORE_0 + variableIndex);
                    break;
                case InstructionConstants.OP_FSTORE:
                    opcode = (byte)(InstructionConstants.OP_FSTORE_0 + variableIndex);
                    break;
                case InstructionConstants.OP_DSTORE:
                    opcode = (byte)(InstructionConstants.OP_DSTORE_0 + variableIndex);
                    break;
                case InstructionConstants.OP_ASTORE:
                    opcode = (byte)(InstructionConstants.OP_ASTORE_0 + variableIndex);
                    break;
            }
        }

        // Only make the instruction wide if necessary.
        wide = requiredVariableIndexSize() > 1 || requiredConstantSize() > 1;

        return this;
    }

    protected boolean isWide() {
        return wide;
    }

    protected void readInfo(byte[] code, int offset) {
        int variableIndexSize = variableIndexSize();
        int constantSize = constantSize();

        // Also initialize embedded variable indexes.
        if (variableIndexSize == 0) {
            // An embedded variable index can be decoded as follows.
            variableIndex = opcode < InstructionConstants.OP_ISTORE_0 ? (opcode - InstructionConstants.OP_ILOAD_0) & 3
                : (opcode - InstructionConstants.OP_ISTORE_0) & 3;
        } else {
            variableIndex = readValue(code, offset, variableIndexSize);
            offset += variableIndexSize;
        }

        constant = readSignedValue(code, offset, constantSize);
    }

    protected void writeInfo(byte[] code, int offset) {
        int variableIndexSize = variableIndexSize();
        int constantSize = constantSize();

        if (requiredVariableIndexSize() > variableIndexSize) {
            throw new IllegalArgumentException(
                "Instruction has invalid variable index size (" + this.toString(offset) + ")");
        }

        if (requiredConstantSize() > constantSize) {
            throw new IllegalArgumentException("Instruction has invalid constant size (" + this.toString(offset) + ")");
        }

        writeValue(code, offset, variableIndex, variableIndexSize);
        offset += variableIndexSize;
        writeSignedValue(code, offset, constant, constantSize);
    }

    public int length(int offset) {
        return (wide ? 2 : 1) + variableIndexSize() + constantSize();
    }

    public void accept(Clazz clazz, Method method, CodeAttribute codeAttribute, int offset,
        InstructionVisitor instructionVisitor) {
        instructionVisitor.visitVariableInstruction(clazz, method, codeAttribute, offset, this);
    }

    // Implementations for Object.

    public String toString() {
        return getName() + (wide ? "_w" : "") + " v" + variableIndex + (constantSize() > 0 ? ", " + constant : "");
    }

    // Small utility methods.

    /**
     * Returns the variable index size for this instruction.
     */
    private int variableIndexSize() {
        return (opcode >= InstructionConstants.OP_ILOAD_0 && opcode <= InstructionConstants.OP_ALOAD_3)
            || (opcode >= InstructionConstants.OP_ISTORE_0 && opcode <= InstructionConstants.OP_ASTORE_3) ? 0
                : wide ? 2 : 1;
    }

    /**
     * Computes the required variable index size for this instruction's variable index.
     */
    private int requiredVariableIndexSize() {
        return (variableIndex & 0x3) == variableIndex ? 0
            : (variableIndex & 0xff) == variableIndex ? 1 : (variableIndex & 0xffff) == variableIndex ? 2 : 4;

    }

    /**
     * Returns the constant size for this instruction.
     */
    private int constantSize() {
        return opcode != InstructionConstants.OP_IINC ? 0 : wide ? 2 : 1;
    }

    /**
     * Computes the required constant size for this instruction's constant.
     */
    private int requiredConstantSize() {
        return opcode != InstructionConstants.OP_IINC ? 0
            : (byte)constant == constant ? 1 : (short)constant == constant ? 2 : 4;
    }
}
